2019 SDC 议题回顾 | Android容器和虚拟化
自Android诞生以来,凭借其系统开源且免费的特性,受到了不少用户和手机玩家的喜爱。
由于用户的应用场景和需求也在时刻变化,只安装了一个操作系统的智能手机愈来愈无法满足用户日益复杂的个性化需求,同时也缺乏对用户隐私数据的保护。
原生 Android 系统的市占率不断下滑,越来越多的定制 Android 系统占领了手机市场。各种新功能层出不穷。那么Android 容器有哪些新玩法呢?
下面就让我们来回顾看雪2019安全开发者峰会上《Android容器和虚拟化》的精彩内容。
编辑按
crownless:目前,通过容器技术和虚拟化技术,可以实现安卓APP多开、定制、代码注入、进行各种控制,这给DIY甚至是灰产、黑产开辟了新的可能性,而且可以躲避一些杀毒软件对恶意代码的查杀。为了实现容器,需要对底层的技术有着深刻的理解,而且要考虑到易用程度。容器技术不仅是一门技术,也是一门艺术!
嘉宾介绍
邓维佳(ID:virjar),毕业于四川大学软件工程专业。目前致力于Android安全相关技术研究,包括App加固脱壳、Android群控技术、Android多开容器等。
讲师围绕Android容器,介绍了目前开源的容器方案和实现,展现了多种思路,通过他的演讲,观众深入理解了多开机制,以及如何通过容器的方式,绕过安全软件的检测等等。
演讲具体内容
以下为速记全文:
最近几年有挺多多开的实现,很多厂商都实现了,比如一些APP的多开或者多环境的功能,它们能实现我们对安卓手机里同一个APP能够存在登陆两个帐号,在营销等有很多应用。这里比较流行的一个东西就是VA。
这张图是2016年开源的框架,这个框架里能够对同样一个APP实现无限的分身,我们如果用来做灰产,或者通过这个模式对APP里面的逻辑进行挟持、控制,是非常方便的。
我觉得VA是非常优秀的框架,在这里它实现对安卓系统很多功能的类似于系统层面的包装,作者安卓的了解已经出神入化了。
最近在安卓的hook等各种方面的发展,可能有其他思路可以把VA功能做更多扩展。做安全的都了解VA里有两个功能,就像多开跟简单的控制。
我们做什么?先看看VA架构图,有同学把VA流程图、架构图做了一个梳理,但是因为我这里想对VA有其他功能的变种,所以我这个图跟大家认可的图不一样,主要是三个技术点,是VA本身的机理理念有三个最重要的模块:
一个是IO重定向。IO重定向能实现多种空间隔离的最基本原理,就是能通过对应文件系统的访问,实现Relocation。
第二个模块是插桩子系统。对于安卓原生来说,所有组件必须在Android清单文件里配置,所以如果是在安卓基本组件不去注册的话,那么它是没办法去执行的。
但是在VA里面我们实现免安装去打开一个APP,它是怎么实现的?主要是通过插桩,在里面配置各种没有实际意义的、但就是去系统里占位的一些配置项。
然后在VA里面,它是以这种插装的形式去系统申请资源,当资源申请到之后,它需要去对插装里的各种配置进行还原,比如把一个StubActivity还原成一个真实的Activity。
另外一块是系统服务模拟,基本把SystemServer这一层做一次模拟,包括安卓系统的各种Manager,所以它有一个进程,这个进程代替了安卓的SystemServer的角色,这个进程非常复杂。所以,在VA里面我们能够通过这个工具、这个APP、这样一个用户操作的APP界面,实现分身、多开。
毕竟VA是在一个虚拟的环境里,我们能不能把这个VA模拟成单层的像一个APP一样?比如我某个APP,然后通过VA,把它的图标等等看起来跟跟原始APP一样,就能够实现我对这个APP进行定制、进行代码注入、进行各种控制化,比如把广告去掉,或者我们加自己的广告,或者加自己的密码拦截,坏事好事等等。
这个APP如果变成一个普通APP、普通APP,对于C端用户来说,做转化之后他看不出跟原始APP有区别。
所以VA本身看起来是有一个用户界面需要去点击、安装、做各种配置,如果我把VA去进行一个包装之后,让VA的容器环境跟VA的内嵌的APK打包成一个的话,就变成现在这个样子。
来看这张图,这个图是拿着line做实验,能看到上一张图是在VA本身环境里,在APP里面进行的各种APP的打开、安装等等。看这张图,它其实是在安卓的VA的外面,我们看到这个图里有多个line的图标。
所以对VA的分身,可以看这个VA本身一个APK里面进行分身,除此之外,还有一种方式实现在APP层面的分身。做成这个样子之后,它跟APP本身的痕迹可能就有些不一样了,当然了,有其它的好处,我是哪一个APP,把APP处理、把APP分身,那么就是它的漏洞。
所以如果我对现在流行那些的APP进行这样的操作,然后实现一些代码,最后实现各种控制,其实对于普通用户来说,他是分辨不了的。
比如我们公司写了一个APP,有些功能限制,我做一些插件、做一些什么东西之后,我能够把这个发放给一些普通人,这个时候它是不是做灰产、做黑产,或者做些DIY,这种大家都了解的东西。
另外可以进行有害的、有病毒的APP的隐藏,因为这个在VA里面我们的APP可以实现免安装的运行。在免安装运行之后,所有APK的一些,比如有害的痕迹在VA里是一个文件,这个文件可以进行转储、进行加密、进行压缩等等。
这种情况下通过杀毒软件、通过代码痕迹的扫描,因为它只是一个资源,这时候常规的黄、赌、毒那些大家没想到的有害的APP都可以经过这种方式包装,包装之后基本上有害杀毒软件平台对它进行痕迹检测、特征检测也是检测不出来的。
在VA层面上,对刚才的那张图做了两个改造,它要实现同样一个APP进行分身,所以IO层面这个模块依然是需要存在的。这不是这个图里面的,第一个APP应该是在进程外实现的单个APP单纯的转发。
除此之外,是我们把Stub系统的插装模块给去掉了,为什么?因为在VA里面它是需要免安装一些运行APK,但是我如果拿已知的APK跟VA融合的,那么这个APK本身的配置文件我们是知晓的,所以这时根本不需要做插装。
为什么这么做?因为这个时候我们是已知的,本身对VA来说,它对插装模块有一些对原生系统的兼容性问题不是很好处理,我们把这个模块去掉以后,那一块兼容性基本不需要考虑了。
还一个是对于VA里的SystemServer的模拟的服务层,服务层在我们这也不需要了,为什么?我们现在对一个APP包装就是VA的引擎加上我们自己的,VA引擎包括单个APP,我们说分身是在那个,APP是在外面分身的,这时我不需要再托管系统的各种功能、activity栈等,全都不需要了,这都可以抹掉了,抹掉之后我也不需要去考虑。
对于VA原生来说,它的开源版本从8.0之后基本就不怎么支持了,主要是它需要一直去跟随安卓本身框架源码的变化,然后做些自己的patch、做自己的功能实现。如果我把这个模块也拿掉的话,我也不需要考虑这一层兼容性的处理了。
但是这一层也还需要一点点,需要什么?VA宿主apk的package和运行内部的apk所见的Package是不一样的,package是安卓里面唯一定位的一个APP的标志。也就是说VA对于系统来说,它是VA的package,但是对于它内部运行的APP来说,它是自己的package。
所以如果它想调用系统功能,比如想查看系统某个包的信息、调用其他APP的API,要经过SystemServer的调用,这时对于应用来说,他自己的包名发送给SystemServer时,SystemServer肯定认为这是一个不合法的包名。
所以我们还需要有一个简单的模块,这个模块是package的Transefer。比如我这是io.m.app,然后我内部一个APP是com.a.b,com.a.b去调用API时,它肯定传递到自己的package,就是com.a.b,然后对于安卓系统来说,它能见到的是io.m.app。
所以我们需要这样一个包的转换的模块功能。基于这个改造以后,上面那张图就能实现了,相对来说它的痕迹,至少未来插装系统是不需要了,SystemServer兼容性也不需要了。相对应的这两层,我们做反VA、反容器的痕迹也会发生变化,在这种情况下,除了开始的io重定向的痕迹现在还没抹掉。
还有一点是签过签名,因为对于安卓系统来说,它看到的签名是宿主的签名,但是对于内部APP来说,它的签名是它自己的。所以我我们在服务这一层还是要保留签名模拟功能。
当然,这个服务已经不是个进程了,而单纯是个模块了,它需要把原生的、把内部的签名读到,给它解析出来。当APP去获取自己签名时,要返回apk自己真实的签名。
这是我现在研究的对VA的一种变形,但是这种改造方式是有问题的,就是它一定会存在两个package的问题,为什么两个package?比如在外围、在控制层面它是.io.virtual.app,但是对于它是com.a.b。com.a.b它到APP上自己去读的时候,它所见到的文件路径应该是” /data/data/自己的包名”,而不是”/data/data/.io.virtual.app”,但实际上这个文件夹在安卓系统是不存在的,也没有权限读写的,它能读写的只是” /data/data/.io.virtual.app”。
那怎么办?所以文件的这个模块是不存在的。我们想,能不能让它存在?就是宿主的包名跟内部运行APP的包名完全一致的话,是不是可以?我是不是能把这个做得更像一点?
这是我后来发现的第二种思路,我们用加壳的思路去做这个容器。什么叫加壳?加壳其实就是把原来别人的APP变成一些资源,然后做些加密,把APP的一些入口的信息替换成自己的,先运行壳代码,壳代码运行了之后把资源解密出来,解密出来之后把相关的数据还原、相关的流程还原,然后开始正常APK的流程。
如果我让这个壳APK在我的这个容器环境内运行的话,我是不是也能在有壳APP运行入口之前先运行我的代码,然后我在把我的坏事做完之后,然后再把运行的控制权交给壳程序,让它脱壳,壳脱完之后再进行业务逻辑的运行。所以我在壳的外面如果再套一层壳,是不是也能实现这些注入控制?
所以在存在加壳的情况下,如果对他进行重打包、功能改造、代码修改是基本不现实的,但是如果我让这个APP运行在我的环境里,然后我用动态注入的方式,当这个APP运行在内存里,我在进行一部分劫持,其实也是另一种重打包的思路,这就是第二种思路。
这个时候看这张图,黄色的地方是原生APK的资源,我会把我们的框架,就是我们自己壳外部的资源,跟原生我想处理的合并的这个APK资源进行融合,它们融合的方式就是把清单文件里的入口修改成我们自己的入口,classes.dex是安卓的代码资源文件,我把这个classes.dex所有的代码文件全部替换成我自己的,再把这个Manifest的文件入口修正到我自己这里。
这样APP运行时肯定会先运行我外面这个入口代码,外层这个APP运行之后,我去把资源文件里面原生APK数据都进来,加载之后进行对象替换,以及各种模拟加壳加固的思路,把所有切换完之后,再把这个控制权交给APK。那么它是有壳,它自己脱壳,它自己运行正常业务逻辑。
但是这个地方有一个问题。为什么我们能够去修改这个AndroidManifest.xml文件?现在很多APK是修改不了AndroidManifest.xml文件的,因为大部分有反资源重打包,有资源混淆对抗等等。
而且这个AndroidManifest.xml跟这个resource.arsc文件,它们是同时出现的,它们都是通过安卓aapt进行打包输出的。通过apktool解存在资源混淆对抗的包的资源文件的话,一般都会失败的。
但我这为什么能改?其实原理很简单,就是我并不需要把AndroidManifest.xml解成文本格式,以及把这个resource.arsc资源解成对应的资源文件,都不需要解。在安卓Manifest文件里,它其实就是一个安卓ARSC的格式,在二进制层面我们可以往它的字符串常量池里加一些常量,在二进制层面去修改它的数据内容。
我们现在不考虑签名问题的话,其实在二进制层面修改,能够把我们自己入口的classes配置修正到这个AndroidManifest里面去。AndroidManifest.xml文件一定是需要被安卓系统解析的,所以它一定是不会存在这个层面的对抗,再怎么混淆也不可能混淆到安卓系统无法识别,所以安卓系统能读,那么我这也就能读、能改。
但是这种方式也是有一些弊端的,我们看看这张图的最终效果,这是我们用套个壳的方式去运行APK,它有几秒等待时间。这跟VA本身的运行机理一样的,VA如果去打开一个没有安装的APP,它也是会有几分钟时间的等待。为什么?在安卓5之后,所有的APK在安装的时候需要有一个过程,需要把这个DEX转成OAT的格式,这个过程在安装过程中需要等待,小APP可能就很快,稍微大点的APP可能需要等5分钟以上。
在这种套壳容器方案里面,如果我们同步去等待这个资源的加载过程,大APP会有5、6分钟的卡屏,这个卡屏对我们用户来说不是很友好的,所以我们需要做一个跳转的页面。像刚才咱们需要做一个跳转,这其实跟VA本身其实是一样的,VA本身也是有一个安装过程,只是大家在VA里去安装看apk,可以方便的现实进度条,看起来足够友好。
能明显的看到,用这个方案实现容器的话,一定需要做首页的跳转。所以在刚才那个AndroidManifest.xml里面,我们第一个是把入口代码修正到我们的入口去,第二是我们需要插入一个页面,当页面运行时我们需要一步的去进行dex2oat,当它执行完成之后回调,我们才把原生APP的入口打开。
这样有一个好处,第一个,APK体积会小很多,相对代码入口重编译来说,它只需要存在外面壳的自己的几个dex,很多资源是不需要的,所以说不会存在APK膨胀的问题,原来的这种方式打包的APP基本上也还是那么大,但刚才说的是资源的对抗是不需要的,因为我们是在二进制层面直接修改Manifest的入口,我根本没有对它进行资源的解包。
但是它有一个contentprovider的问题,我们都知道当appication初始化之后、启动之后,它会安装contentprovider,但是因为我们的contentprovider配置到Manifest里面面,那么它一定会进行contentprovider的安装过程。
因为当我们的代码没有run起来之前,没有把内部资源APK运行起来之前,我们是没办法拿到的classLoader。这是如果强行去安装contentprovider的话,它是会报ClassNotFoundException的。
当然,很多时候contentprovider可能会需要同步调用,我们这个只能做成异步,因为它有延时,做成异步的话,对于调用方可能看到在报错。但是如果我只是单纯对这一个APP放到我们容器里进行分析、进行插件的定制的话,其实对我们来说也是无所谓的。
基本的流程就是这个样子,首先,APK启用的时候会跑到我们这里来,我们第一步需要把contentprovider安装给hook掉,就是我们不能让它在这时进行contentprovider安装。contentprovider安装时,先把contentprovider信息收集起来,但是我们现在先拒绝掉,第二步是判断dex是不是需要同步加载,如果打开一个页面时就同步加载内部容器内资源的话,它会卡屏几分钟,这时用户肯定无法等待的。
这时如果不能同步加载,我们需要先打开activity,在activity里面启动异步线程,然后在进行APK的load,load之后如果是同步的话,比如后台线程以及一些service,这些其他的在后台的组件的话,那我们同步进行安装处理。以及如果我之前曾经打开过这个,它其实已经进行了安装,这时我不需要再进行异步的。完成之后,我们把classLoader替换掉。
classLoader替换掉之后,把资源也重置一下,指向我们自己媒资里面另一个原生APK的地址,它会把资源也替换成原生APK的东西。然后我们再正常流程,模拟安卓启动的过程,把appication创造起来,去模拟它的生命周期,再把contentprovider启动。这时候ClassLoader已经出现了,那么contentprovider可以安装了,流程到这时,对原生APK来说,它看到的可能就以为是正常的流程。
这是第二种思路,这个类似于VA的环境,然后在内部它是一个没有安装的APK的资源。所以本身跟VA很将近,但是跟VA相比来说,大家可以想象一下,如果你在VA里在安装一个VA是会有问题的,因为底层安卓的各种service底层有大量对象以package作为ID。然后我们这种方式实现了只有一个pacakge,就是容器外面的package跟我所运行apk资源的package是完全一样的。
第三种思路就是multiDex。在4.4以前一个APK文件一般只有一个Dex,但是我们业务会比较大,比如我们代码越来越多,就会超过65535以上的限制,APK足够大时需要进行dex分包,就需要存在multidex,但这个功能在4.4以前是需要单独写逻辑去兼容的,但是在5.0以后这个multidex被安卓原生控制了。
如果我能够修改Manifest入口的话,那我往这个APK里去附加一个我自己的dex文件,然后再把Manifest里那个入口指向我自己、指向我的代码。先运行我们的代码,代码运行之后,再把控制权交回原生的一些逻辑,通过这种思路也是能实现注入的。
但这种注入相对于我们重打包来说,我是没有去修改dex的内容,所以对原生APK来说,它如果存在对抗逻辑的话,如果它去检查dex的签名或者dex文件指纹,也是检查不出来什么东西的,因为我根本没对它进行任何修改。当然基于这个思路做的,是不会有启动加载延时。
大家看到这个地方我用了XposedAPI,大家做安卓安全的都了解这个框架,我通过这个实现java代码的hook。能看到我这个是能跳出来的,就是右边代码,左边能看出来它这个。
不管通过哪个手段,能够把Xposed的一些功能实现引进来,这时至少在java层能够hook所有java逻辑。所以我基于重打包的方式,通过写java代码,进行一些钩子函数的代码注入。其实动态hook实现重打包和源码级别重打包相比,第一种更加方便。
然后我们把插件代码跟原生想要攻击apk合成一个APK,它就是单独的另一个产品,比如广告去掉之后做DIY。
大概的思路就是下面这张图,这张图里所有资源都是我想要攻击的APP的资源,除开在classes里面新增了一个dex文件,这个dex文件是没自己入口的逻辑。我修改改的只有AndroidManifest文件里那个<applicationname=”entry”>,只能从二进制层面去修改它的入口。和刚刚一样,二进制层面修改AndroidManifest文件一定能够成功,不存在所谓的资源对抗。
最后一种思路是入口修改,为什么我把这个放在最后讲?因为现在已经有比较成熟的方案,我把dex入口层面的代码进行重编译,比如在壳代码之前插入bootstrap指令,然后把这个代码流程转到我们定制的逻辑里面去,进行我们的逻辑代码执行之后再返回控制流。这个其实有几个已经实现了。比如太极,xpatch。
通过对AndroidManifest的解析,我们找到入口是那个dex,找到dex以后找到它的代码文件,如果它是appication的话,在attachBaseContext的时候,加入我们的smali指令,或者如果它是MainActivity,在MainActivity的attachBaseContext加入插入smali指令。
但这有一个问题,有一个6535的问题,就是如果我进行dex文件的重新编译的话,我们是增加了新的代码到入口dex里面,我们说的是dex文件本身里面各种编码都是两个字节,各种字符串的数量全是在两个字节,如果这个dex的某个数量已经是快接近于6万多,如果我再新增一坨东西放到那个dex里,它一旦超过了6535就会失败了。
所以这个地方需要插入的东西,只能是一个浅层的,加入一个启动代码,这个启动代码仅仅是为了加载媒资里我们自己的dex或者我们自己的APK资源。这样我可能只需要加1个类,然后2、3个方法,实现媒资里apk自动解码,classesLoader的创建。这个APK加的东西很少,这种情况下不太有可能出现6535的限制问题。
如果说我把dex文件进行修改的话,它dex文件的特征一定会有些变化,所以如果我进行dex文件检测的话就能检测出来,但实际也是不对的,为什么?因为在安全的世界里,你hook我也能hook,如果我把这个io重定向做了,让她读到了真实的apk资源就可以。
所以是这个流程,我们在入口进行重编译,因为我们重打包,签名是在所有容器里、所有沙箱里都需要去做的,所以其中一个重要功能就是对packageManager进行hook,实现签名拦截,以及所有他们可能检测的点、资源需要进行重定向,甚至把我们增加特征抹除掉,然后再加载我们自己的插件,然后再到正常的启动流程。这个针对于大部分APK来说也是能做到成功的。
整理一下,左边是怎么攻,右边是怎么防,因为我们进行重打包,PackageManager是需要我们hook的,它有两种思路,一种是用代理的方式实现,一种是通过hook的方式,我其实是能够去hook ART的一些东西。
如果它存在一些文件签名,我通过重定向的方式让它读到真的,然后各种拦截,maps等各种都能伪造。最主要的就是两个,就是第6条,我通过二进制层面去修改入口,能够对抗资源混淆,但是代理痕迹是classLoader,classLoader不是指向/data/app的,而是指向/data/宿主里面的。
还有就是针对于dex重编译的话,dex特征发生变化,可以在oat文件中找到特征,这个最主要是在maps里OAT文件,通过maps的内存描述能够读取到。然后可以检查oat特征的。
注意:点击文末原文链接,即可查看本议题完整演讲PPT。其他议题演讲PPT,经过讲师同意后会陆续放出,请大家持续关注看雪论坛及看雪学院公众号!
2、2019 SDC 议题回顾 | 新威胁对策:TSCM 技术反窃密
3、2019 SDC 议题回顾 | 安全研究视角看macOS平台EDR安全能力建设
4、2019 SDC 议题回顾 | 基于云数据的司法取证技术
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com
↙点击“阅读原文”,下载演讲PPT